// Copyright (c) 2022 Presto Labs Pte. Ltd.
// Author: qiang

#include "coin2/exchange/crypto_common/api_util/rate_tracker.h"

#include <algorithm>
#include <sstream>
#include <string>

#include <boost/algorithm/string/replace.hpp>
#include <glog/logging.h>

#include "coin2/base/proto_util.h"
#include "coin2/exchange/base/order/rate_config.h"

namespace coin2::exchange::crypto_common::api_util {
constexpr auto EXAMPLE_SPOT_CRYPTO = R"(
"connection_config": {
  "key_filepath": "/home/ec2-user/config/trade/xunke18/crypto/trade_key.json",
  "rate_limit_info": {
    "Spot.Crypto.v1": {
      "fill_order": {
        "secs": 1,
        "reqs": 1
      },
      "account": {
        "secs": 1,
        "reqs": 30
      },
      "open_order": {
        "secs": 1,
        "reqs": 30
      },
      "place_order": {
        "secs": 1,
        "reqs": 150
      },
      "cancel_order": {
        "secs": 1,
        "reqs": 150
      },
      "hist_order": {
        "secs": 1,
        "reqs": 1
      }
    }
  }
},)";

constexpr auto EXAMPLE_FUTURES_CRYPTO = R"(
"connection_config": {
  "key_filepath": "/home/ec2-user/config/trade/xunke18/crypto/trade_key.json",
  "rate_limit_info": {
    "Futures.Crypto.v1": {
    "fill_order": {
        "secs": 1,
        "reqs": 1
      },
      "account": {
        "secs": 1,
        "reqs": 30
      },
      "open_order": {
        "secs": 1,
        "reqs": 30
      },
      "place_order": {
        "secs": 1,
        "reqs": 150
      },
      "cancel_order": {
        "secs": 1,
        "reqs": 150
      },
      "hist_order": {
        "secs": 1,
        "reqs": 1
      },
      "position": {
        "secs": 1,
        "reqs": 30
      }
  }
  }
},)";

template <class RateTracker>
void CheckFail(RateTracker&& rate_tracker, bool check_result) {
  if (!check_result) {
    LOG(INFO) << "Hit rate limit on [" << rate_tracker->GetName() << "]";
  }
}

CryptoRateTracker::CryptoRateTracker(
    const MarketExchangeApi& mea,
    const coin2::exchange::order::OrderConnectionConfig& cfg)
    : mea_{mea} {
  using RateConfig = coin2::exchange::base::order::RateConfig;
  if (mea.market == coin::proto::MarketType::Spot) {
    RateLimitInfoCryptoV1 rate_config;
    RateConfig::Get()->GetConfig(mea, cfg, &rate_config);
    InitRateTracker(&rate_config, mea.String(), EXAMPLE_SPOT_CRYPTO);
  } else if (mea.market == coin::proto::MarketType::Futures) {
    RateLimitInfoCryptoV1 rate_config;
    RateConfig::Get()->GetConfig(mea, cfg, &rate_config);
    CHECK(rate_config.has_position());
    InitRateTracker(&rate_config, mea.String(), EXAMPLE_FUTURES_CRYPTO);
  } else {
    CHECK(false) << "unsupported mea: " << mea.String();
  }
}

void CryptoRateTracker::InitRateTracker(
    RateLimitInfoCryptoV1* rate_config,
    std::string_view mea_str,
    std::string_view example) {
  if (!rate_config || !rate_config->has_balance() || !rate_config->has_hist_order() ||
      !rate_config->has_fill_order() || !rate_config->has_open_order() ||
      !rate_config->has_place_order() || !rate_config->has_cancel_order()) {
    LOG(INFO) << "mea_str " << mea_str << "  example " << example;
    coin2::exchange::base::order::RateConfig::CheckFail(mea_str, example);
  }

  const auto& balance = rate_config->balance();
  const auto& history = rate_config->hist_order();
  const auto& open_order = rate_config->open_order();
  const auto& place_order = rate_config->place_order();
  const auto& cancel_order = rate_config->cancel_order();
  const auto& fill_order = rate_config->fill_order();

  rate_tracker_account_ =
      std::make_unique<FixedWindowRateTracker>(balance.secs() * 1'000'000'000LL);
  rate_tracker_account_->SetRateLimitInfo(balance.reqs(), balance.reqs());
  rate_tracker_account_->SetName("crypto_rate_tracker_req_balance(1 sec)");

  rate_tracker_history_ =
      std::make_unique<FixedWindowRateTracker>(history.secs() * 1'000'000'000LL);
  rate_tracker_history_->SetRateLimitInfo(history.reqs(), history.reqs());
  rate_tracker_history_->SetName("crypto_rate_tracker_req_hisorder(1 sec)");

  rate_tracker_openorder_ =
      std::make_unique<FixedWindowRateTracker>(open_order.secs() * 1'000'000'000LL);
  rate_tracker_openorder_->SetRateLimitInfo(open_order.reqs(), open_order.reqs());
  rate_tracker_openorder_->SetName("crypto_rate_tracker_req_openorder(1 sec)");

  rate_tracker_placeorder_ =
      std::make_unique<FixedWindowRateTracker>(place_order.secs() * 1'000'000'000LL);
  rate_tracker_placeorder_->SetRateLimitInfo(place_order.reqs(), place_order.reqs());
  rate_tracker_placeorder_->SetName("crypto_rate_tracker_req_placeorder(1 sec)");

  rate_tracker_cancelorder_ =
      std::make_unique<FixedWindowRateTracker>(cancel_order.secs() * 1'000'000'000LL);
  rate_tracker_cancelorder_->SetRateLimitInfo(cancel_order.reqs(), cancel_order.reqs());
  rate_tracker_cancelorder_->SetName("crypto_rate_tracker_req_cancelorder(1 sec)");

  rate_tracker_fillorder_ =
      std::make_unique<FixedWindowRateTracker>(fill_order.secs() * 1'000'000'000LL);
  rate_tracker_fillorder_->SetRateLimitInfo(fill_order.reqs(), fill_order.reqs());
  rate_tracker_fillorder_->SetName("crypto_rate_tracker_req_fillorder(1 sec)");

  std::stringstream stream;
  stream << "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
         << "\nOG request rate was set as below:\n"
         << "\nbalance: [" << balance.reqs() << "(reqs)/" << balance.secs() << "(secs)]"
         << "\nhis_order: [" << history.reqs() << "(reqs)/" << history.secs() << "(secs)]"
         << "\nfill_order: [" << fill_order.reqs() << "(reqs)/" << fill_order.secs() << "(secs)]"
         << "\nopen_order: [" << open_order.reqs() << "(reqs)/" << open_order.secs() << "(secs)]"
         << "\nplace_order: [" << place_order.reqs() << "(reqs)/" << place_order.secs() << "(secs)]"
         << "\ncancel_order: [" << cancel_order.reqs() << "(reqs)/" << cancel_order.secs()
         << "(secs)]";

  if (rate_config->has_position()) {
    const auto& position = rate_config->position();
    rate_tracker_position_ =
        std::make_unique<FixedWindowRateTracker>(position.secs() * 1'000'000'000LL);
    rate_tracker_position_->SetRateLimitInfo(position.reqs(), position.reqs());
    rate_tracker_position_->SetName("crypto_rate_tracker_req_position_by_ids(1 sec)");
    stream << "\nposition: [" << position.reqs() << "(reqs)/" << position.secs() << "(secs)]";
  }
  stream << "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~";

  LOG(INFO) << std::string(stream.str());
  rate_limit_report_writer_ = std::make_unique<RateLimitReportWriter>();
  rate_limit_report_writer_->SetRateLimitInfoString(rate_config);
}

bool CryptoRateTracker::HasEnoughRateLimit(
    int64_t timestamp,
    ActionType action_type,
    QueryType query_type) {
  bool ret = true;
  if (action_type == ActionType::PLACE) {
    if (rate_tracker_placeorder_) {
      ret = rate_tracker_placeorder_->GetRemaining(timestamp) > 2;
      CheckFail(rate_tracker_placeorder_, ret);
    }
  } else if (action_type == ActionType::CANCEL) {
    if (rate_tracker_cancelorder_) {
      ret = rate_tracker_cancelorder_->GetRemaining(timestamp) > 2;
      CheckFail(rate_tracker_cancelorder_, ret);
    }
  } else if (action_type == ActionType::QUERY) {
    if (query_type == QueryType::ACCOUNT) {
      if (rate_tracker_account_) {
        ret = (rate_tracker_account_->GetRemaining(timestamp) > 2);
        CheckFail(rate_tracker_account_, ret);
      }
    } else if (query_type == QueryType::OPEN_ORDER) {
      if (rate_tracker_openorder_) {
        ret = (rate_tracker_openorder_->GetRemaining(timestamp) > 2);
        CheckFail(rate_tracker_openorder_, ret);
      }
    } else if (query_type == QueryType::HIST_ORDER) {
      if (rate_tracker_history_) {
        ret = (rate_tracker_history_->GetRemaining(timestamp) > 2);
        CheckFail(rate_tracker_history_, ret);
      }
    } else if (query_type == QueryType::FILL_ORDER) {
      if (rate_tracker_fillorder_) {
        ret = (rate_tracker_fillorder_->GetRemaining(timestamp) > 2);
        CheckFail(rate_tracker_fillorder_, ret);
      }
    } else if (query_type == QueryType::POSITION) {
      if (rate_tracker_position_) {
        ret = (rate_tracker_position_->GetRemaining(timestamp) > 2);
        CheckFail(rate_tracker_position_, ret);
      }
    } else {
      LOG(ERROR) << "[HasEnoughRateLimit] Unknown query type: " << query_type;
    }
  } else {
    LOG(ERROR) << "[HasEnoughRateLimit] Unknown action type: " << action_type;
  }
  if (!ret) {
    rate_limit_report_writer_->SetHitRateLimit(timestamp, action_type);
  }
  return ret;
}

void CryptoRateTracker::Push(
    int32_t num_reqs,
    int64_t timestamp,
    ActionType action_type,
    QueryType query_type) {
  if (action_type == ActionType::PLACE) {
    if (rate_tracker_placeorder_) {
      rate_tracker_placeorder_->Push(num_reqs, timestamp);
    }
  } else if (action_type == ActionType::CANCEL) {
    if (rate_tracker_cancelorder_) {
      rate_tracker_cancelorder_->Push(num_reqs, timestamp);
    }
  } else if (action_type == ActionType::QUERY) {
    if (query_type == QueryType::ACCOUNT) {
      if (rate_tracker_account_) {
        rate_tracker_account_->Push(num_reqs, timestamp);
      }
    } else if (query_type == QueryType::OPEN_ORDER) {
      if (rate_tracker_openorder_) {
        rate_tracker_openorder_->Push(num_reqs, timestamp);
      }
    } else if (query_type == QueryType::HIST_ORDER) {
      if (rate_tracker_history_) {
        rate_tracker_history_->Push(num_reqs, timestamp);
      }
    } else if (query_type == QueryType::FILL_ORDER) {
      if (rate_tracker_fillorder_) {
        rate_tracker_fillorder_->Push(num_reqs, timestamp);
      }
    } else if (query_type == QueryType::POSITION) {
      if (rate_tracker_position_) {
        rate_tracker_position_->Push(num_reqs, timestamp);
      }
    } else {
      LOG(ERROR) << "[Push] Unknown query type: " << query_type;
    }
  } else {
    LOG(ERROR) << "[Push] Unknown action type: " << action_type;
  }
}

void CryptoRateTracker::SetRateLimitInfoString(const google::protobuf::Message* message) {
  if (!rate_limit_report_writer_) {
    return;
  }
  rate_limit_report_writer_->SetRateLimitInfoString(message);
}

void CryptoRateTracker::WriteRateLimitReportMessage(OrderLoggerCB callback) {
  if (!callback) {
    return;
  }
  rate_limit_report_writer_->Write(callback);
}

double CryptoRateTracker::GetMaxRateUsedRatio(int64_t timestamp, ActionType action_type) {
  switch (action_type) {
    case ActionType::PLACE: {
      return GetMaxRateUsedRatio(rate_tracker_placeorder_, timestamp);
    }
    case ActionType::CANCEL: {
      return GetMaxRateUsedRatio(rate_tracker_cancelorder_, timestamp);
    }
    case ActionType::QUERY: {
      double balance_rate_used_ratios = GetMaxRateUsedRatio(rate_tracker_account_, timestamp);
      double openorder_rate_used_ratios = GetMaxRateUsedRatio(rate_tracker_openorder_, timestamp);
      double allorder_rate_used_ratios = GetMaxRateUsedRatio(rate_tracker_history_, timestamp);
      double fillorder_rate_used_ratios = GetMaxRateUsedRatio(rate_tracker_fillorder_, timestamp);
      auto semifinal = std::max(
          std::max(balance_rate_used_ratios, openorder_rate_used_ratios),
          std::max(allorder_rate_used_ratios, fillorder_rate_used_ratios));
      if (rate_tracker_position_) {
        return std::max(semifinal, GetMaxRateUsedRatio(rate_tracker_fillorder_, timestamp));
      }
      return semifinal;
    }
    default: {
      // shouldn't reached here
      LOG(ERROR) << "ops~ something is wrong, action_type=" << action_type;
      return 0.;
    }
  }
}

}  // namespace coin2::exchange::crypto_common::api_util
